﻿using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Collections.Generic;

namespace GwLib.JqGridMvc.Extensions
{
    public static class ObjectExtensions
    {
        private static List<MemberInfo> GetSortedMembers(object o)
        {
            var t = o.GetType();
            var sb = new StringBuilder();

            var mis = new List<MemberInfo>();
            mis.AddRange(t.GetFields().Where(f => f.IsPublic));
            mis.AddRange(t.GetProperties().Where(f => f.CanRead));
            mis = mis.OrderBy(m => m.Name).ToList();
            return mis;
        }

        public static string ToCsvHeaderString(this object o)
        {
            var first = true;
            var sb = new StringBuilder();
            var mis = GetSortedMembers(o);

            foreach (MemberInfo mi in mis)
            {
                sb.Append(first ? "" : ",").Append("\"").Append(mi.Name).Append("\"");
                first = false;
            }

            return sb.ToString();
        }
        public static string ToCsvString(this object o)
        {
            var first = true;
            var sb = new StringBuilder();
            var mis = GetSortedMembers(o);

            foreach (MemberInfo mi in mis)
            {
                if (!first) sb.Append(",");
                else first = false;
                object val = null;
                if (mi is FieldInfo) val = (mi as FieldInfo).GetValue(o);
                else if (mi is PropertyInfo) val = (mi as PropertyInfo).GetGetMethod().Invoke(o, new object[] { });

                bool isNull = val == null || val == DBNull.Value;
                if (!isNull)
                {
                    var baseType = val.GetType();
                    if (val.IsNullableValueType() || val.IsNullableType())
                    {
                        isNull = !(bool)val.GetType().GetProperty("HasValue").GetGetMethod().Invoke(o, new object[] {});
                        val = val.GetType().GetProperty("Value").GetGetMethod().Invoke(o, new object[] {});
                        baseType = val.AutoResolveNullable();
                    }
                    
                    if (!isNull)
                    {
                        sb.Append("\"");
                        if (baseType == typeof(DateTime))
                        {
                            sb.Append(((DateTime)val).ToString("u"));
                        }
                        else
                        {
                            sb.Append(val.ToString().Replace("\"", "\"\""));
                        }
                        sb.Append("\"");
                    }
                }
            }

            return sb.ToString();
        }

        public static string ToIniString(this object o, bool showNullProperties = false)
        {
            var t = o.GetType();
            var sb = new StringBuilder();
            sb.Append("[").Append(t.FullName).Append("]").AppendLine();
            foreach (FieldInfo fi in t.GetFields().Where(f=>f.IsPublic))
            {
                var fo = fi.GetValue(o);
                if (showNullProperties || fo != null)
                {
                    sb.Append(fi.Name).Append(".Type=").Append(fi.FieldType.FullName).AppendLine();
                    sb.Append(fi.Name).Append("=");
                    if (fo == null)
                        sb.Append("<NULL>").AppendLine();
                    else
                        sb.Append("\"").Append(fo.ToString().Replace("\"", "\"\"")).Append("\"").AppendLine();
                }
            }
            foreach (var pi in t.GetProperties().Where(p => p.CanRead))
            {
                var po = pi.GetGetMethod().Invoke(o, new object[] { });
                if (showNullProperties || po != null)
                {
                    sb.Append(pi.Name).Append(".Type=").Append(pi.PropertyType.FullName).AppendLine();
                    sb.Append(pi.Name).Append("=");
                    if (po == null)
                        sb.Append("<NULL>").AppendLine();
                    else
                        sb.Append("\"").Append(po.ToString().Replace("\"", "\"\"")).Append("\"").AppendLine();
                }
            }
            return sb.ToString();
        }
        /*
        public static object PropertyData(this object obj, IBasicColumnDefinition def)
        {
            Type objType = obj.GetType();
            var getMethod = def.MappedProperty.GetGetMethod();
            object data = getMethod.Invoke(obj, null);
            return data;
        }*/

        public static object PropertyData(this object obj, string propertyName)
        {
            Type objType = obj.GetType();
            PropertyInfo propertyInfo = objType.GetProperty(propertyName);
            var getMethod = propertyInfo.GetGetMethod();
            object data = getMethod.Invoke(obj, null);
            return data;
        }

        public static object PropertyData<T>(this object obj, string propertyName)
        {
            Type objType = obj.GetType();
            PropertyInfo propertyInfo = objType.GetProperty(propertyName);
            var getMethod = propertyInfo.GetGetMethod();
            object data = getMethod.Invoke(obj, null);
            return (T)data;
        }

        public static T DeepCopy<T>(this T o) where T : new()
        {
            T copy = new T();
            DeepCopyObject(o, copy);
            return copy;
        }

        public static T Copy<T>(this object o) where T : new()
        {
            T copy = new T();
            CopyMatchingProperties(o, copy);
            return copy;
        }

        public static void CopyMatchingPropertiesInto<T>(this T o, object target) where T : class
        {
            CopyMatchingProperties(o, target);
        }

        public static bool DeepCompare<T>(this T o, T o2) where T : new()
        {
            return DeepCompareObjects(o, o2);
        }

        public static bool IsNullableType(this object o)
        {
            Type t = (o is Type) ? t = o as Type : o.GetType();
            return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>);
        }

        public static bool IsNullableValueType(this object o)
        {
            Type t = o.GetType();
            Type[] ga = t.GetGenericArguments();
            return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)
                && ga.Length == 1 && (ga[0].IsPrimitive || ga[0].IsValueType);
        }

        public static Type AutoResolveNullable(this object o)
        {
            Type t = (o is Type) ? t = o as Type : o.GetType();
            if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                return t.GetGenericArguments()[0];
            }
            else
            {
                return t;
            }
        }

        public static void CopyMatchingProperties(object source, object target)
        {
            if (source != null)
            {
                Type t = source.GetType();
                Type ct = target.GetType();
                if (target == null)
                {
                    var constr = FindMatchingConstructor(t);
                    if (constr != null) target = constr.Invoke(Type.EmptyTypes);
                }

                if (target != null)
                {
                    if (source is IList)
                    {
                        IList copyList = target as IList,
                            list = source as IList;
                        foreach (object item in list)
                        {
                            var itemType = item.GetType();
                            if (itemType.IsPrimitive || itemType.IsValueType || item.IsNullableValueType() || typeof(string) == itemType)
                            {
                                copyList.Add(item);
                            }
                            else if (item != null)
                            {
                                var constr = FindMatchingConstructor(itemType);
                                if (constr != null)
                                {
                                    var itemCopy = constr.Invoke(Type.EmptyTypes);
                                    CopyMatchingProperties(item, itemCopy);
                                    copyList.Add(itemCopy);
                                }
                            }
                        }
                    }
                    else
                    {
                        foreach (FieldInfo f in t.GetFields())
                        {
                            var tf = ct.GetFields().Where(cf => cf.Name == f.Name && cf.FieldType.AutoResolveNullable() == f.FieldType.AutoResolveNullable()).FirstOrDefault();
                            if (tf != null)
                            {
                                CopyMember(f, source, tf, target);
                            }
                        }
                        foreach (var p in t.GetProperties())
                        {
                            var tp = ct.GetProperties().Where(cp => cp.Name == p.Name && cp.PropertyType.AutoResolveNullable() == p.PropertyType.AutoResolveNullable()).FirstOrDefault();
                            if (tp != null)
                            {
                                if (p.CanWrite && p.CanRead)
                                {
                                    CopyMember(p, source, tp, target);
                                }
                            }
                        }
                    }
                }
            }
        }

        private static ConstructorInfo FindMatchingConstructor(Type t)
        {
            ConstructorInfo c = null;
            if (t.IsInterface)
            {
                Type matchingType = null;
                if (t.IsGenericType)
                {
                    if (t.GetGenericTypeDefinition() == typeof(IList<>))
                        matchingType = Type.GetType(t.FullName.Replace("IList", "List"));
                    else if (t.GetGenericTypeDefinition() == typeof(ICollection<>))
                        matchingType = Type.GetType(t.FullName.Replace("ICollection", "List"));
                    else if (t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                        matchingType = Type.GetType(t.FullName.Replace("IEnumerable", "List"));
                    else if (t.GetGenericTypeDefinition() == typeof(IDictionary<,>))
                        matchingType = Type.GetType(t.FullName.Replace("IDictionary", "Dictionary"));
                }
                else
                {
                    matchingType = AppDomain.CurrentDomain.GetAssemblies().ToList()
                        .SelectMany(s => s.GetTypes())
                        .FirstOrDefault(p => t.IsClass && !t.IsAbstract && t.IsAssignableFrom(p) && t.GetConstructor(Type.EmptyTypes) != null);
                }
                if (matchingType != null)
                {
                    c = matchingType.GetConstructor(Type.EmptyTypes);
                }
            }
            else
            {
                c = t.GetConstructor(Type.EmptyTypes);
            }
            return c;
        }

        public static void DeepCopyObject(object o, object copy)
        {
            if (o != null)
            {
                Type t = o.GetType();
                if (copy == null)
                {
                    var constr = FindMatchingConstructor(t);
                    if (constr != null) copy = constr.Invoke(Type.EmptyTypes);
                }
                else if (copy.GetType() != t)
                {
                    throw new Exception("Cannot copy from type " + t.Name + " to type " + copy.GetType().Name);
                }

                if (copy != null)
                {
                    if (o is IList)
                    {
                        IList copyList = copy as IList,
                            list = o as IList;
                        foreach (object item in list)
                        {
                            var itemType = item.GetType();
                            if (itemType.IsPrimitive || itemType.IsValueType || item.IsNullableValueType() || typeof(string) == itemType)
                            {
                                copyList.Add(item);
                            }
                            else if (item != null)
                            {
                                var constr = FindMatchingConstructor(itemType);
                                if (constr != null)
                                {
                                    var itemCopy = constr.Invoke(Type.EmptyTypes);
                                    DeepCopyObject(item, itemCopy);
                                    copyList.Add(itemCopy);
                                }
                            }
                        }
                    }
                    else
                    {
                        foreach (FieldInfo f in t.GetFields())
                        {
                            CopyMember(f, o, copy);
                        }
                        foreach (var p in t.GetProperties())
                        {
                            if (p.CanWrite && p.CanRead)
                            {
                                CopyMember(p, o, copy);
                            }
                        }
                    }
                }
            }
        }

        private static void CopyMember(MemberInfo m, object o, object copy)
        {
            CopyMember(m, o, m, copy);
        }

        private static void CopyMember(MemberInfo m, object o, MemberInfo cm, object copy)
        {
            object data;
            Type mt = (m is PropertyInfo) ? ((PropertyInfo)m).PropertyType : ((FieldInfo)m).FieldType,
                dt = (cm is PropertyInfo) ? ((PropertyInfo)cm).PropertyType : ((FieldInfo)cm).FieldType;

            if (m is PropertyInfo) data = ((PropertyInfo)m).GetGetMethod().Invoke(o, new object[] { });
            else data = ((FieldInfo)m).GetValue(o);

            if (mt.IsPrimitive || mt.IsValueType || (data != null && data.IsNullableValueType()) || typeof(string) == mt)
            {
                if (mt.IsNullableType() && !dt.IsNullableType())
                {
                    if (data == null) data = Activator.CreateInstance(dt);
                    else data = Convert.ChangeType(data, dt);
                }
                if (cm is PropertyInfo) ((PropertyInfo)cm).GetSetMethod().Invoke(copy, new object[] { data });
                if (cm is FieldInfo) ((FieldInfo)cm).SetValue(copy, data);
            }
            else if (data != null)
            {
                var constr = FindMatchingConstructor(dt);
                if (constr != null)
                {
                    var dataCopy = constr.Invoke(Type.EmptyTypes);
                    DeepCopyObject(data, dataCopy);
                    if (cm is PropertyInfo) ((PropertyInfo)cm).GetSetMethod().Invoke(copy, new object[] { dataCopy });
                    if (cm is FieldInfo) ((FieldInfo)cm).SetValue(copy, data);
                }
            }
        }

        public static bool DeepCompareObjects(object o1, object o2)
        {
            if (o1 == null && o2 == null) return true;
            else if (o1 == null || o2 == null) return false;
            else if (o1.GetType() != o2.GetType()) return false;
            else
            {
                Type t = o1.GetType();

                if (o1 is IEnumerable)
                {
                    IEnumerator 
                        l1 = (o1 as IEnumerable).GetEnumerator(), 
                        l2 = (o2 as IEnumerable).GetEnumerator();

                    bool
                        n1 = l1.MoveNext(),
                        n2 = l2.MoveNext();
                    if (n1 != n2) return false;

                    while (n1 && n2)
                    {
                        if (!CompareObject(l1.Current, l2.Current)) return false;
                        
                        n1 = l1.MoveNext();
                        n2 = l2.MoveNext();
                        if (n1 != n2) return false;
                    }
                }
                else
                {
                    foreach (FieldInfo f in t.GetFields())
                    {
                        if (!CompareMember(f, o1, o2)) return false;
                    }
                    foreach (var p in t.GetProperties())
                    {
                        if (!CompareMember(p, o1, o2)) return false;
                    }
                }
            }
            return true;
        }

        private static bool CompareMember(MemberInfo m, object o1, object o2)
        {
            object data1, data2;
            Type mt = (m is PropertyInfo) ? ((PropertyInfo)m).PropertyType : ((FieldInfo)m).FieldType;

            if (m is PropertyInfo)
            {
                data1 = ((PropertyInfo)m).GetGetMethod().Invoke(o1, new object[] { });
                data2 = ((PropertyInfo)m).GetGetMethod().Invoke(o2, new object[] { });
            }
            else
            {
                data1 = ((FieldInfo)m).GetValue(o1);
                data2 = ((FieldInfo)m).GetValue(o2);
            }

            return CompareObject(data1, data2);
        }

        private static bool CompareObject(object data1, object data2)
        {
            if (data1 == null && data2 == null) return true;
            else if (data1 == null || data2 == null) return false;
            else if (data1.GetType() != data2.GetType()) return false;
            else
            {
                Type t = data1.GetType();
                if (t.IsPrimitive || t.IsValueType || data1.IsNullableValueType() || typeof(string) == t)
                {
                    return object.Equals(data1, data2);
                }
                else
                {
                    return DeepCompareObjects(data1, data2);
                }
            }
        }
    }
}